using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Text;
using STATCONNECTORSRVLib;
using StatConnectorCommonLib;
using System.Reflection;
using System.Query;
using System.Management.Automation;
using System.Management.Automation.Provider;
using System.Runtime.InteropServices;

namespace PowerShell2R.Interop
{
    public class RInterop: IDisposable 
    {
        private ErrorDevice _errorDevice = new ErrorDevice();

        /// <summary>
        /// Passed data objects
        /// </summary>
        // private List<PSObject> _data = new List<PSObject>();
        private IEnumerable<PSObject> _data;

        /// <summary>
        /// Proxy to R
        /// </summary>
        internal StatConnectorClass _r;

        public RInterop(IEnumerable<PSObject> data)
        {
            _data = data;
            _r = new StatConnectorClass();
            // NOT IMPLEMENTED :(
            // _r.SetErrorDevice(_errorDevice); 
            // some minor change added something
            // yet another line
            _r.Init("R");
        }

        public void Dispose()
        {
            if(_r != null)
            {
                Marshal.FinalReleaseComObject(_r);
                _r = null;
            }
        }

        //public void Add(PSObject obj)
        //{
        //    _data.Add(obj);
        //}

        public PSObject Evaluate(string expression)
        {
            try
            {
                if(_data != null)
                {
                    // collect the data
                    Dictionary<string, List<object>> dataFrame = new Dictionary<string, List<object>>();

                    foreach (PSObject obj in _data)
                    {
                        foreach (PSPropertyInfo pi in obj.Properties)
                        {
                            if (pi.IsGettable && pi.Value != null)
                            {
                                List<object> column;
                                if (!dataFrame.ContainsKey(pi.Name))
                                {
                                    column = new List<object>();
                                    dataFrame.Add(pi.Name, column);
                                }
                                else
                                    column = dataFrame[pi.Name];

                                PSObject psObj = pi.Value as PSObject;
                                if(psObj != null)
                                    column.Add(psObj.BaseObject);
                                else
                                    column.Add(pi.Value);
                            }
                        }
                    }


                    //var unusedSymbols = new List<string>();
                    // pass it to R
                    foreach (var column in dataFrame)
                    {
                        Type elementType = column.Value[0].GetType();

                        // R doesn't support certain types
                        if(elementType == typeof(long) || elementType == typeof(Int64)) 
                        {
                            // elementType = typeof(double); // lets get something bigger
                            var obj = (from v in column.Value select Convert.ToDouble(v)).ToArray();
                            _r.SetSymbol(column.Key, obj);
                        }
                        else
                        {
                            Array arr = Array.CreateInstance(elementType, column.Value.Count);
                            Array.Copy(column.Value.ToArray(), arr, arr.Length);

                            _r.SetSymbol(column.Key, arr);
                        }
                    }

                    // construct data.frame
                    //string dataFrameExpr = string.Format("d = data.frame({0})",
                    //    // join( , ",")
                    //    // dataFrame.Keys.Except(unusedSymbols).Fold(( keys, nextKey) => string.Format("{0},{1}", keys, nextKey))));
                    //    dataFrame.Keys.Fold(( keys, nextKey) => string.Format("{0},{1}", keys, nextKey)));

                    // here as an issue with uppercase, lowercase, and somehow with types
                    // plus error messages suck... maybe the COM thing has to be enhanced
                    // _r.EvaluateNoReturn(dataFrameExpr);
                }

                // * is.array
                // * is.vector // logical, integer, real, complex, character, raw 
                // * is.real   // Double
                // * is.factor // enum
                // * is.list   // properties
                // * ?is.integer
                // * is.data.frame  // row and column access
                // * is.matrix      // maybe the same
                // * is.character // is irgendwie komisch

                return AsPSObject(expression, 0);
            }
            catch (COMException comEx)
            {
                throw new PSInvalidOperationException(comEx.Message + " " + _r.GetErrorText() + " id: " + _r.GetErrorId());
            }
        }

        internal PSObject AsPSObject(string expression, int level)
        {
            string res = string.Format("res{0}", level);
            _r.EvaluateNoReturn(string.Format("{0} = {1}", res, expression));

            // build check expression
            string primitiveCheckExpression = new string[]{"is.double", "is.real", "is.logical", "is.integer"}
                .Select(op => string.Format("{0}({1})", op, res))
                .Fold( (op,nextOp) => string.Format("{0} | {1}", op, nextOp));

            if((bool)_r.Evaluate(primitiveCheckExpression)) 
                return PSObject.AsPSObject(_r.Evaluate(res));
            else if((bool)_r.Evaluate(string.Format("is.data.frame({0})",res)))
            {
                PSObject psObject = new PSObject();

                // produce properties for each column
                foreach(var colName in (string[])_r.Evaluate(string.Format("colnames({0})", res)))
                {
                    // lets get the column
                    object column = _r.Evaluate(string.Format("{0}[,\"{1}\"]", res, colName));
                    psObject.Properties.Add(new PSNoteProperty(colName, PSObject.AsPSObject(column))); 
                }

                // unit tests...
                // bug fix single values
                // eval-r "lm(y~x,data=data.frame(x=1:3,y=2:4))"
                //ls *xml|where {$_.length -lt 50000} | add-member -membertype aliasproperty -name foo -value length -passthru | select foo, name | eval-r "ls()"

                //ls | select length, @{name="foo";expression={$_.lastwritetime.ticks}} | eval-r d

                // produce properties for each column 
                // (lets see what happens with numbers as rownames!!!!111)
                foreach(var rowName in (string[])_r.Evaluate(string.Format("rownames({0})", res)))
                {
                    // lets get the column
                    object row = _r.Evaluate(string.Format("{0}[\"{1}\",]", res, rowName));
                    psObject.Properties.Add(new PSNoteProperty(rowName, PSObject.AsPSObject(row))); 
                }

                return psObject;
            }
            else if((bool)_r.Evaluate(string.Format("is.list({0})", res)))
            {
                PSObject psObject = new PSObject();
                // produce properties for each column
                // foreach(var name in (string[])_r.Evaluate(string.Format("names({0})[apply({0}, ]", expression)))
                // make sure we don't get an empty list
                string listNames = string.Format(
                    "names({0})[sapply(names({0}), function(name) {{ !is.list({0}[[name]]) || length({0}[[name]]) > 0 }})]", res);
                foreach(var name in (string[])_r.Evaluate(listNames))
                {
                    // lets get the item
                    object item = AsPSObject(string.Format("{0}[[\"{1}\"]]", res, name), level+1);
                    // it maybe that we can't convert it
                    if(item != null)
                        psObject.Properties.Add(new PSNoteProperty(name, PSObject.AsPSObject(item))); 
                }

                return psObject;
            }
            else
                return null;
                // return PSObject.AsPSObject(_r.Evaluate(string.Format("str({0})", res)));
        }
    }

    /// <summary>
    ///  Lazy loading. This might be interesting...
    /// </summary>
    //public class RProperty : PSPropertyInfo
    //{
    //    private RInterop _interop;
    //    private string _expression;

    //    public RProperty(RInterop interop, string expression, string name)
    //    {
    //        _interop = interop;
    //        _expression = expression;
    //        // base.Name = name;
    //    }

    //    public override Object Value 
    //    { 
    //        get { return _interop.AsPSObject(_expression, /* fix me *7); } 
    //        set { /* TODO */ } 
    //    }

    //    public override bool IsGettable { get { return true; } }
    //    public override bool IsSettable { get { return false; } }
    //    public override PSMemberInfo Copy() { return this; }
    //    public override PSMemberTypes MemberType { get { return PSMemberTypes.Property;} }
    //    public override string TypeNameOfValue { get { return "not for now"; } }
    //}

    //public class RProviderBase: CmdletProvider
    //{
    //    private Interop _interop;
    //    private string _expression;

    //    protected RProviderBase(Interop interop, string expression)
    //    {
    //        _interop = interop;
    //        _expression = expression;
    //    }

    //    protected StatConnectorClass R { get { return _interop._r; } }

    //    protected string Expression { get { return _expression; } }
    //}

    //[CmdletProvider( "R data.frame Provider", ProviderCapabilities.None )]
    //public class DataFrameProvider : RProviderBase, IPropertyCmdletProvider
    //{
    //    public DataFrameProvider(Interop interop, string expression) : base(interop, expression)
    //    { }

    //    public void GetProperty(string path, Collection<string> providerSpecificPickList)
    //    {
    //        PSObject psObject = new PSObject();

    //        // produce properties for each column
    //        foreach(var colName in (string[])R.Evaluate(string.Format("colnames({0})", expression)))
    //        {
    //            // lets get the column
    //            object column = R.Evaluate(string.Format("{0}[,\"{1}\"]", expression, colName));
    //            psObject.Properties.Add(new PSNoteProperty(colName, PSObject.AsPSObject(column))); 
    //        }

    //        // produce properties for each column 
    //        // (lets see what happens with numbers as rownames!!!!111)
    //        foreach(var rowName in (string[])R.Evaluate(string.Format("rownames({0})", expression)))
    //        {
    //            // lets get the column
    //            object row = R.Evaluate(string.Format("{0}[\"{1}\",]", expression, colName));
    //            psObject.Properties.Add(new PSNoteProperty(rowName, PSObject.AsPSObject(row))); 
    //        }

    //        WritePropertyObject(psObject, path);
    //    }

    //    public object GetPropertyDynamicParameters(string path, Collection<string> providerSpecificPickList) { return null; }
    //    public void SetProperty(string path, PSObject propertyValue) { }
    //    public object SetPropertyDynamicParameters(string path, PSObject propertyValue) { return null; }
    //    public void ClearProperty(string path, Collection<string> propertyToClear) { }
    //    public object ClearPropertyDynamicParameters(string path, Collection<string> propertyToClear) { return null; }
    //}

    internal class ErrorDevice: IStatConnectorCharacterDevice
    {
        internal StringBuilder ErrorText = new StringBuilder();

        public void Clear()
        {
        }

        public void WriteString(string bstrLine)
        {
            ErrorText.AppendLine(bstrLine);
        }
        
        public void WriteStringLevel(string bstrLine, int lLevel)
        {
            for (int i = 0; i < lLevel; i++)
                ErrorText.Append("  ");
            ErrorText.AppendLine(bstrLine);
        }
    }
}
